﻿namespace Home.Services.ProductServices
{
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.Serialization;
    using System.ServiceModel;
    using System.ServiceModel.Activation;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Transactions;
    using CsvHelper;
    using CsvHelper.Configuration;
    using Home.Models;
    using Home.Repository;
    using Home.Security;
    using Home.Wcf;

    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
    [ServiceBehavior(Name = WebsiteServiceHelper.ProductWebServiceName, Namespace = WebsiteServiceHelper.WebServiceNamespace, ConcurrencyMode = ConcurrencyMode.Multiple, IgnoreExtensionDataObject = true, InstanceContextMode = InstanceContextMode.PerCall)]
    [WcfErrorHandlingBehavior(Namespace = WebsiteServiceHelper.WebServiceNamespace)]
    public class ProductService : IProductService
    {
        private const string ErrorCodeUploadRequestIdAlreadyExists = "request_id_duplicate";
        private const string ErrorCodeUploadRequestIdAlreadyExistsReason = "The request id is duplicate. It is already existing.";
        private const string ErrorCodeUploadRequestIdWrongFormat = "request_id_wrong_format";
        private const string ErrorCodeUploadRequestIdWrongFormatReason = "The format of the request id is invalid.";
        private const string ErrorCodeUploadInvalidIndex = "invalid_index";
        private const string ErrorCodeUploadInvalidIndexReason = "The index value is invalid. It either does not exist or duplicate.";
        private const string ErrorCodeUploadInvalidProductType = "invalid_product_type";
        private const string ErrorCodeUploadInvalidProductTypeReason = "The product type is invalid.";
        private const string ErrorCodeUploadInvalidProductUploadType = "invalid_product_upload_type";
        private const string ErrorCodeUploadInvalidProductUploadTypeReason = "The product upload type is invalid.";
        private const int UploadFieldPositionIndex = 0;
        private const int UploadFieldPositionCost = 1;
        private const int UploadFieldPositionSalePrice = 2;
        private const int UploadFieldPositionProduct = 3;

        public async Task Upload(string id, ProductType productType, ProductUploadType uploadType, string data)
        {
            var now = DateTimeOffset.UtcNow;
            var user = Thread.CurrentPrincipal.Identity.Name;
            var requestId = await ValidateRequestAsync(id, productType, uploadType, now, user);

            if (string.IsNullOrEmpty(data))
            {
                return;
            }

            var transaction = default(TransactionScope);
            var database = default(ProductDbContext);
            var stringReader = default(StringReader);
            var csvReader = default(CsvReader);
            try
            {
                stringReader = new StringReader(data);
                csvReader = new CsvReader(stringReader, new CsvConfiguration()
                {
                    Delimiter = ",",
                    HasHeaderRecord = false,
                    IgnoreHeaderWhiteSpace = false,
                    Quote = '\"',
                    QuoteAllFields = false,
                    QuoteNoFields = false,
                    ThrowOnBadData = true,
                    TrimFields = true,
                    Encoding = Encoding.Unicode,
                });
                transaction = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions() { IsolationLevel = IsolationLevel.ReadCommitted }, TransactionScopeAsyncFlowOption.Enabled);
                database = new ProductDbContext();

                var upload = new ProductUpload()
                {
                    Id = Guid.NewGuid(),
                    ProductType = productType,
                    UploadType = uploadType,
                    RequestId = requestId,
                    Created = now,
                    CreatedBy = user,
                    Modified = now,
                    ModifiedBy = user,
                };
                database.Uploads.Add(upload);
                await database.SaveChangesAsync();

                var provider = default(UploadProductProvider);
                switch (productType)
                {
                    case ProductType.Diamond:
                        provider = new UploadDiamondProvider();
                        break;
                }

                switch (uploadType)
                {
                    case ProductUploadType.Replace:
                        await database.Database.ExecuteSqlCommandAsync(string.Concat("DELETE ", provider.TableName));
                        goto case ProductUploadType.Append;

                    case ProductUploadType.Append:
                        while (csvReader.Read())
                        {
                            UploadAppend(upload, provider, database, csvReader, now, user);
                        }
                        break;

                    case ProductUploadType.Update:
                        {
                            var products = (IEnumerable<Product>)await QueryableExtensions.ToArrayAsync(provider.DbSet(database));
                            while (csvReader.Read())
                            {
                                var product = FindProduct(csvReader, products);
                                if (product == null)
                                {
                                    throw new FaultException(ErrorCodeUploadInvalidIndexReason, new FaultCode(ErrorCodeUploadInvalidIndex));
                                }

                                UploadUpdate(product, upload, provider, csvReader, now, user);
                            }
                        }
                        break;

                    case ProductUploadType.AppendOrUpdate:
                        {
                            var products = (IEnumerable<Product>)await QueryableExtensions.ToArrayAsync(provider.DbSet(database));
                            while (csvReader.Read())
                            {
                                var product = FindProduct(csvReader, products);
                                if (product == null)
                                {
                                    UploadAppend(upload, provider, database, csvReader, now, user);
                                }
                                else
                                {
                                    UploadUpdate(product, upload, provider, csvReader, now, user);
                                }
                            }
                        }
                        break;
                }

                await database.SaveChangesAsync();
                transaction.Complete();
            }
            finally
            {
                if (csvReader != null) csvReader.Dispose();
                if (stringReader != null) stringReader.Dispose();
                if (database != null) database.Dispose();
                if (transaction != null) transaction.Dispose();
            }
        }

        private static async Task<Guid> ValidateRequestAsync(string id, ProductType productType, ProductUploadType uploadType, DateTimeOffset now, string user)
        {
            var result = Guid.NewGuid();

            var key = default(string);
            var date = default(DateTime);
            var guid = default(Guid);
            if (!SecurityHelper.TryParseRequestId(id, out date, out guid, out key))
            {
                throw new FaultException(ErrorCodeUploadRequestIdWrongFormatReason, new FaultCode(ErrorCodeUploadRequestIdWrongFormat));
            }

            var today = now.Date;
            if (date > today)
            {
                throw new FaultException(ErrorCodeUploadRequestIdWrongFormatReason, new FaultCode(ErrorCodeUploadRequestIdWrongFormat));
            }
            else if (date < today && now.Hour > 0 && now.Minute > 1)
            {
                throw new FaultException(ErrorCodeUploadRequestIdWrongFormatReason, new FaultCode(ErrorCodeUploadRequestIdWrongFormat));
            }

            using (var db = new UtilityDbContext())
            {
                if (await db.Requests.AnyAsync(u => u.Date == date && u.Guid == guid && u.Type == RequestType.ProductUpload))
                {
                    throw new FaultException(ErrorCodeUploadRequestIdAlreadyExistsReason, new FaultCode(ErrorCodeUploadRequestIdAlreadyExists));
                }

                db.Requests.Add(new RequestDescription()
                {
                    Id = result,
                    Date = date,
                    Guid = guid,
                    Type = RequestType.ProductUpload,
                    Created = now,
                    CreatedBy = user,
                    Modified = now,
                    ModifiedBy = user,
                });
                await db.SaveChangesAsync();
            }

            if (!Enum.IsDefined(typeof(ProductType), productType))
            {
                throw new FaultException(ErrorCodeUploadInvalidProductTypeReason, new FaultCode(ErrorCodeUploadInvalidProductType));
            }

            if (!Enum.IsDefined(typeof(ProductUploadType), uploadType))
            {
                throw new FaultException(ErrorCodeUploadInvalidProductUploadTypeReason, new FaultCode(ErrorCodeUploadInvalidProductUploadType));
            }

            return result;
        }

        private static Product FindProduct(CsvReader reader, IEnumerable<Product> products)
        {
            var index = reader.GetField<int>(0);
            var product = products.FirstOrDefault(p => p.Index == index);
            return product;
        }

        private static void UploadAppend(ProductUpload upload, UploadProductProvider provider, ProductDbContext database, CsvReader reader, DateTimeOffset now, string user)
        {
            var product = provider.New();
            product.Id = Guid.NewGuid();
            product.Created = now;
            product.CreatedBy = user;
            product.Modified = now;
            product.ModifiedBy = user;
            product.Index = reader.GetField<int>(UploadFieldPositionIndex);
            product.Cost = reader.GetField<decimal>(UploadFieldPositionCost);
            product.SalePrice = reader.GetField<decimal>(UploadFieldPositionSalePrice);
            product.Status = ProductStatus.Active;
            product.UploadId = upload.Id;
            provider.SetValues(reader, product);
            //// call DbSet<T>.Add() method with reflection. call Add method with dynamic does not work. the method needs real object type.
            var dbSet = (object)provider.DbSet(database);
            var dbSetType = dbSet.GetType().GetTypeInfo();
            var productType = dbSetType.GetGenericArguments()[0].GetTypeInfo();
            var method = dbSetType.GetMethod("Add", new[] { productType });
            method.Invoke(dbSet, new object[] { product });

            ++upload.Count;
        }

        private static void UploadUpdate(Product product, ProductUpload upload, UploadProductProvider provider, CsvReader reader, DateTimeOffset now, string user)
        {
            if (product.Status == ProductStatus.Active)
            {
                product.Modified = now;
                product.ModifiedBy = user;
                product.Cost = reader.GetField<decimal>(UploadFieldPositionCost);
                product.SalePrice = reader.GetField<decimal>(UploadFieldPositionSalePrice);
                product.Status = ProductStatus.Active;
                product.UploadId = upload.Id;
                provider.SetValues(reader, product);
            }

            ++upload.Count;
        }

        private abstract class UploadProductProvider
        {
            public abstract Product New();
            public abstract void SetValues(CsvReader reader, Product product);
            public abstract dynamic DbSet(ProductDbContext database);
            public abstract string TableName { get; }

            protected static string GetString(CsvReader reader, int index)
            {
                var value = reader.GetField<string>(index);
                return string.IsNullOrEmpty(value) ? default(string) : value;
            }

            protected static T? GetEnum<T>(CsvReader reader, int index)
                where T : struct
            {
                var value = reader.GetField<string>(index);
                return string.IsNullOrEmpty(value) ? new T?() : new T?((T)Enum.Parse(typeof(T), value));
            }

            protected static decimal? GetDecimal(CsvReader reader, int index)
            {
                var value = reader.GetField<string>(index);
                return string.IsNullOrEmpty(value) ? new decimal?() : new decimal?(decimal.Parse(value));
            }

            protected static string GetStringLast(CsvReader reader, int index)
            {
                var value = default(string);
                if (reader.TryGetField<string>(index, out value))
                {
                    return string.IsNullOrEmpty(value) ? default(string) : value;
                }
                return null;
            }
        }

        private class UploadDiamondProvider : UploadProductProvider
        {
            public override Product New()
            {
                return new Diamond();
            }

            public override void SetValues(CsvReader reader, Product product)
            {
                var diamond = (Diamond)product;
                var pos = UploadFieldPositionProduct;
                diamond.Type = GetEnum<DiamondType>(reader, pos++).Value;
                diamond.Shape = GetEnum<DiamondShape>(reader, pos++);
                diamond.ReportType = GetEnum<DiamondReport>(reader, pos++);
                diamond.ReportNumber = GetString(reader, pos++);
                diamond.Caret = GetDecimal(reader, pos++);
                diamond.Clarity = GetEnum<DiamondClarity>(reader, pos++);
                diamond.Color = GetEnum<DiamondColor>(reader, pos++);
                diamond.ColoredColor = GetString(reader, pos++);
                diamond.Cut = GetEnum<DiamondCut>(reader, pos++);
                diamond.Polish = GetEnum<DiamondPolish>(reader, pos++);
                diamond.Symmetry = GetEnum<DiamondSymmetry>(reader, pos++);
                diamond.Fluorescence = GetString(reader, pos++);
                diamond.Comment = GetStringLast(reader, pos++);
            }

            public override dynamic DbSet(ProductDbContext database)
            {
                return database.Diamonds;
            }

            public override string TableName
            {
                get
                {
                    return "t_product_diamond";
                }
            }
        }
    }
}